package control.render;


import gui.RenderPanel;

import java.awt.image.BufferedImage;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.swing.SwingUtilities;

import control.Core;
import control.profiler.Profiler;
import control.profiler.Statistics;
import data.complex.Camera;
import data.hierarchy.World;
import data.render.IRenderMethod;
import data.render.PathTraceRenderMethod;

public class RenderController {
  private BufferedImage bImage;
  private Camera camera;
  private World world;
  private RenderPanel renderPanel;  
  private RenderSetup renderSetup; 
  private double pixelStep;
  private double relativeHeight;
  private IRenderMethod renderMethod;
  private ThreadPoolExecutor threadPool;
  private boolean isRendering = false;
    
  public RenderController(Core core){
    world = core.getWorld();
    camera = world.getCamera();
    setRenderSetup(core.getRenderSetup());
  }
  
  private void setRenderSetup(RenderSetup renderSetup){
    this.renderSetup = renderSetup;
    pixelStep = 2d / renderSetup.getWidth();
    relativeHeight = 1d / renderSetup.getWidth() * renderSetup.getHeight();
  }

  public void render(){
    if(renderSetup.isUnbiased()){
      renderUnbiased();
    }
    else{
      renderBiased();
    }
  }

  public void renderBiased(){
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        Profiler.getInstance().record("Rendering scene...");
      }
    });
    
    isRendering = true;
    camera.initializeCameraPostion();
    
    int queueLength = 
      (renderSetup.getWidth() / renderSetup.getBucketSize() + 1) * 
      (renderSetup.getHeight() / renderSetup.getBucketSize() + 1);
    
    threadPool = new ThreadPoolExecutor(
        renderSetup.getThreadCount(), 
        renderSetup.getThreadCount(), 
        10, 
        TimeUnit.SECONDS, 
        new ArrayBlockingQueue<Runnable>(queueLength));
    
    for(int y = 0; y < renderSetup.getHeight(); y += renderSetup.getBucketSize()){
      for(int x = 0; x < renderSetup.getWidth(); x += renderSetup.getBucketSize()){
        threadPool.execute(generateNewRenderTask(x, y));
      }
    }
  }
  
  public void renderUnbiased(){
    
    camera.initializeCameraPostion();
    camera.setAperature(renderSetup.getAperature());
    
    if(renderSetup.getFocalLength() == RenderSetup.AUTO_FOCUS)
      camera.autoFocus(getRenderMethod().getVoxelSpace());
    else
      camera.setFocus(renderSetup.getFocalLength());
    
    threadPool = new ThreadPoolExecutor(
        renderSetup.getThreadCount(), 
        renderSetup.getThreadCount(), 
        10, 
        TimeUnit.SECONDS, 
        new ArrayBlockingQueue<Runnable>(renderSetup.getThreadCount()));
    
    MonteCarloImage mcImage = new MonteCarloImage(getImage());
    
    for(int i = 0; i < renderSetup.getThreadCount(); i++){
      threadPool.execute(
          new UnbiasedRenderWorker(
              world, mcImage, getRenderMethod(), renderSetup.getFocalLength() == RenderSetup.FOCUS_OFF));
    }
    
    
    try {
      while(true){
        Thread.sleep(500);
        getRenderPanel().repaint();
        Profiler.getInstance().putMessage("Samples per Pixel: " + mcImage.getSamplesPerPixel());
      }
    }
    catch (InterruptedException e) {
      e.printStackTrace();
    }      
    
  }
  
  public synchronized void bucketCompleted(){
    if(threadPool.getActiveCount() == 0 && threadPool.getQueue().size() == 0 && isRendering){
      isRendering = false;
      SwingUtilities.invokeLater(new Runnable() {
        @Override
        public void run() {
          Profiler.getInstance().display();
          Statistics bucketStat = Profiler.getInstance().getStatistics("bucket");
          Profiler.getInstance().putMessage("Avg Bucket Time: " + bucketStat.getAverage() + "ms");
          Profiler.getInstance().putMessage("Max Bucket Time: " + bucketStat.getMaximum() + "ms");
          Statistics pixelStat = Profiler.getInstance().getStatistics("pixel");
          Profiler.getInstance().putMessage("Avg Pixel Time: " + pixelStat.getAverage() + "ms");
          Profiler.getInstance().putMessage("Max Pixel Time: " + pixelStat.getMaximum() + "ms");
        }
      });
    }
  }
  
  private RenderTask generateNewRenderTask(int x, int y){
    int thisBucketWidth = (x + renderSetup.getBucketSize() < renderSetup.getWidth()) ? 
        renderSetup.getBucketSize() : renderSetup.getWidth() - x;
    int thisBucketHeight = (y + renderSetup.getBucketSize() < renderSetup.getHeight()) ? 
        renderSetup.getBucketSize() : renderSetup.getHeight() - y;
    
    BufferedImage subImage = 
      getImage().getSubimage(x, y, thisBucketWidth, thisBucketHeight);
    
    double rayU = x * pixelStep - 1;
    double rayV = y * pixelStep - relativeHeight;
        
    return new RenderTask(
        subImage, rayU, rayV, pixelStep, world, getRenderMethod(), 
        new BucketCompletedAlert(this, x, y, renderSetup.getBucketSize()));
  }
  
  private IRenderMethod getRenderMethod(){
    if(renderMethod == null){
      renderMethod = new PathTraceRenderMethod(world, renderSetup);
    }
    return renderMethod;
  }
  
  public BufferedImage getImage(){
    if(bImage == null || bImage.getWidth() != renderSetup.getWidth() || bImage.getHeight() != renderSetup.getHeight()){
      bImage = new BufferedImage(
          renderSetup.getWidth(), 
          renderSetup.getHeight(), 
          BufferedImage.TYPE_INT_RGB);
    }
    return bImage;
  }
  
  public RenderPanel getRenderPanel(){
    if(renderPanel == null){
      renderPanel = new RenderPanel(this);
    }
    return renderPanel;
  }
}
